package org.xx.armory.spring5.jdbc;

import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.xx.armory.commons.IntegerEnum;
import org.xx.armory.commons.StringEnum;

import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import java.util.Objects;

import static org.xx.armory.commons.StringConverter.parseBoolean;
import static org.xx.armory.commons.TextUtils.ellipse;
import static org.xx.armory.commons.Validators.rejectIfNull;

public abstract class JdbcMapper<T> {
    public static Integer getInteger(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var result = resultSet.getInt(columnIndex);
            return !resultSet.wasNull() ? result : null;
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get int", columnIndex, ex);
        }
    }

    public static int getIntegerValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getInt(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get int", columnIndex, ex);
        }
    }

    public static Long getLong(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var result = resultSet.getLong(columnIndex);
            return !resultSet.wasNull() ? result : null;
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get long", columnIndex, ex);
        }
    }

    public static long getLongValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getLong(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get long", columnIndex, ex);
        }
    }

    public static Short getShort(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var result = resultSet.getShort(columnIndex);
            return !resultSet.wasNull() ? result : null;
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get float", columnIndex, ex);
        }
    }

    public static short getShortValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getShort(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get float", columnIndex, ex);
        }
    }

    public static Double getDouble(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var result = resultSet.getDouble(columnIndex);
            return !resultSet.wasNull() ? result : null;
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get double", columnIndex, ex);
        }
    }

    public static double getDoubleValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getDouble(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get double", columnIndex, ex);
        }
    }

    public static Float getFloat(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var result = resultSet.getFloat(columnIndex);
            return !resultSet.wasNull() ? result : null;
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get float", columnIndex, ex);
        }
    }

    public static float getFloatValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getFloat(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get float", columnIndex, ex);
        }
    }

    public static BigDecimal getBigDecimal(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            return resultSet.getBigDecimal(columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get BigDecimal", columnIndex, ex);
        }
    }

    public static Date getDate(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var object = resultSet.getTimestamp(columnIndex);
            if (resultSet.wasNull() || object == null) {
                return null;
            } else {
                return new Date(object.getTime());
            }
//            final Object object = resultSet.getObject(columnIndex);
//            if (resultSet.wasNull() || object == null) {
//                return null;
//            } else if (object instanceof Date) {
//                return (Date) object;
//            } else if (object instanceof CharSequence) {
//                return parseDate(object.toString(), null);
//            } else if (object instanceof Number) {
//                return new Date(((Number) object).longValue());
//            } else {
//                throw new ResultSetAccessException("Cannot convert " + object.getClass().getTypeName() + " to  " + Date.class.getTypeName(), columnIndex);
//            }
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get date", columnIndex, ex);
        }
    }

    /**
     * 从结果集中获取 {@link Boolean} 类型的对象。
     * <p>指定列的原始数据类型必须是 {@link Boolean}, {@link Number}, 或者 {@link CharSequence}，才可以被转化为 {@link Boolean}类型。</p>
     *
     * @param resultSet
     *         结果集。
     * @param columnIndex
     *         列序号。
     * @return 指定列的数据。
     * @throws NullPointerException
     *         如果参数 {@code resultSet} 是 {@code null}。
     * @throws DataAccessException
     *         如果列序号超出范围，如果从结果集中读取数据出现错误，如果指定列的数据无法被转化为 {@code Boolean} 类型。
     * @see org.xx.armory.commons.StringConverter#parseBoolean(String, Boolean)
     */
    public static Boolean getBoolean(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final Object object = resultSet.getObject(columnIndex);
            if (resultSet.wasNull() || object == null) {
                return null;
            } else if (object instanceof Boolean) {
                return (Boolean) object;
            } else if (object instanceof Number) {
                return ((Number) object).intValue() != 0;
            } else if (object instanceof CharSequence) {
                return parseBoolean(object.toString(), null);
            } else {
                throw new ResultSetAccessException("Cannot convert " + object.getClass().getTypeName() + " to  " + Boolean.class.getTypeName(), columnIndex);
            }
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get boolean", columnIndex, ex);
        }
    }

    public static boolean getBooleanValue(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        final Boolean b = getBoolean(resultSet, columnIndex);
        return b != null ? b : false;
    }

    public static String getString(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final String s = resultSet.getString(columnIndex);
            return !resultSet.wasNull() ? s : "";
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get String", columnIndex, ex);
        }
    }

    public static byte[] getBytes(
            ResultSet resultSet,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var bytes = resultSet.getBytes(columnIndex);
            return !resultSet.wasNull() ? bytes : null;
//            final var blob = resultSet.getBlob(columnIndex);
//            try {
//                if (resultSet.wasNull()) {
//                    return null;
//                }
//
//                var l = Long.min(blob.length(), 1024 * 1024 * 10L);
//                if (l > 0L) {
//                    return blob.getBytes(0, (int) l);
//                } else {
//                    return new byte[0];
//                }
//            } finally {
//                if (blob != null) {
//                    blob.free();
//                }
//            }
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get byte[]", columnIndex, ex);
        }
    }

    /**
     * 从结果集中获取枚举。
     *
     * @param <T>
     *         枚举的类型。
     * @param resultSet
     *         结果集。
     * @param columnIndex
     *         列序号。
     * @param clazz
     *         枚举的类型。该类型必须实现 {@link IntegerEnum} 接口或者 {@link StringEnum} 接口。
     * @return 指定列的数据。
     * @throws IllegalArgumentException
     *         如果参数 {@code resultSet} 是 {@code null}。如果类型 {@code T} 既未实现 {@link IntegerEnum} 也未实现 {@link StringEnum}。
     * @throws DataAccessException
     *         如果列序号超出范围，如果从结果集中读取数据出现错误，如果指定列的数据无法被转化为指定的枚举类型。
     */
    public static <T extends Enum<T>> T getEnum(
            ResultSet resultSet,
            Class<T> clazz,
            int columnIndex
    )
            throws DataAccessException {
        rejectIfNull(resultSet, "resultSet");

        if (IntegerEnum.class.isAssignableFrom(clazz)) {
            return clazz.cast(getIntegerEnum(resultSet, clazz.asSubclass(IntegerEnum.class), columnIndex));
        } else if (StringEnum.class.isAssignableFrom(clazz)) {
            return clazz.cast(getStringEnum(resultSet, clazz.asSubclass(StringEnum.class), columnIndex));
        } else {
            throw new IllegalArgumentException(clazz.getTypeName() + " is neither IntegerEnum nor StringEnum");
        }
    }

    private static IntegerEnum getIntegerEnum(
            ResultSet resultSet,
            Class<? extends IntegerEnum> clazz,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var iv = resultSet.getInt(columnIndex);
            if (resultSet.wasNull()) {
                throw new ResultSetAccessException("Cannot convert null to " + clazz.getTypeName(), columnIndex);
            }
            for (final var ev : clazz.getEnumConstants()) {
                if (ev.value() == iv) {
                    return ev;
                }
            }
            throw new ResultSetAccessException("Cannot convert number " + iv + " to " + clazz.getTypeName(), columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get " + clazz.getTypeName(), columnIndex, ex);
        }
    }

    private static StringEnum getStringEnum(
            ResultSet resultSet,
            Class<? extends StringEnum> clazz,
            int columnIndex
    )
            throws DataAccessException {
        try {
            final var sv = resultSet.getString(columnIndex);
            if (resultSet.wasNull()) {
                throw new ResultSetAccessException("Cannot convert null to " + clazz.getTypeName(), columnIndex);
            }
            for (final var ev : clazz.getEnumConstants()) {
                if (Objects.equals(ev.stringValue(), sv)) {
                    return ev;
                }
            }
            throw new ResultSetAccessException("Cannot convert string " + ellipse(sv, 32) + " to " + clazz.getTypeName(), columnIndex);
        } catch (SQLException ex) {
            throw new ResultSetAccessException("Cannot get " + clazz.getTypeName(), columnIndex, ex);
        }
    }

    public abstract ResultSetExtractor<T> single();

    public abstract ResultSetExtractor<List<T>> all();

    public abstract ResultSetExtractor<List<T>> page(
            int startRowIndex,
            int maximumRows
    );

    public final ResultSetExtractor<List<T>> page(
            Pageable pageable
    ) {
        rejectIfNull(pageable, "pageable");

        return pageable.isPaged() ? page((int) pageable.getOffset(), pageable.getPageSize()) : all();
    }
}
